package org.kazuhiko.rpg.core;

import static org.lwjgl.opengl.GL11.GL_BACK;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_COLOR_MATERIAL;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_DIFFUSE;
import static org.lwjgl.opengl.GL11.GL_FRONT;
import static org.lwjgl.opengl.GL11.GL_LIGHT0;
import static org.lwjgl.opengl.GL11.GL_LIGHTING;
import static org.lwjgl.opengl.GL11.GL_LIGHT_MODEL_AMBIENT;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW_MATRIX;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_POSITION;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.GL_PROJECTION_MATRIX;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_SMOOTH;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glClear;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glColorMaterial;
import static org.lwjgl.opengl.GL11.glCullFace;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glGetFloat;
import static org.lwjgl.opengl.GL11.glLight;
import static org.lwjgl.opengl.GL11.glLightModel;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glLoadMatrix;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glShadeModel;
import static org.lwjgl.opengl.GL11.glTexCoord2f;
import static org.lwjgl.opengl.GL11.glVertex2f;
import static org.lwjgl.opengl.GL15.glDeleteBuffers;

import java.awt.Rectangle;
import java.nio.FloatBuffer;
import java.text.DecimalFormat;

import org.kazuhiko.rpg.core.types.MenuType;
import org.kazuhiko.rpg.core.types.TextureType;
import org.kazuhiko.rpg.display.GameScreen;
import org.kazuhiko.rpg.display.TextRenderer;
import org.kazuhiko.rpg.display.menu.MainMenu;
import org.kazuhiko.rpg.display.menu.MenuButton;
import org.kazuhiko.rpg.manager.MenuManager;
import org.kazuhiko.rpg.manager.TextureManager;
import org.kazuhiko.rpg.objects.Cube;
import org.kazuhiko.rpg.objects.Player;
import org.kazuhiko.rpg.util.BufferTools;
import org.kazuhiko.rpg.util.ImagingTools;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.vector.Vector3f;
import org.newdawn.slick.opengl.Texture;

public class Game
{
	//The formatter, for formatting decimals
	private DecimalFormat formatter = new DecimalFormat( "#.##" );
	
	//If we have to draw the FPS to the screen
	private boolean drawFPS;
	
	//If we should draw the menu
	private boolean showMenu;
	
	//The last FPS
	private long lastFPS;
	//The last frame
	private long lastFrame;
	
	//The current FPS
	private int currentFPS;
	//The FPS
	private int fps;
	//The max fps
	private int maxFPS;
	
	private FloatBuffer perspectiveProjectionMatrix = BufferTools.reserveData( 16 );
	private FloatBuffer orthographicProjectionMatrix = BufferTools.reserveData( 16 );
	
	//The world
	private World world;
	
	//The camera
	private Camera camera;
	
	//The player
	private Player player;
	private String username;
	
	private static Game instance;
	
	public static Game getGame()
	{
		if( instance == null )
		{
			instance = new Game();
		}
		
		return instance;
	}
	
	private Game()
	{
		init();

	}
	
	private void init()
	{
		lastFPS = 0;
		lastFrame = 0;
		currentFPS = 0;
		maxFPS = 60;
		
		drawFPS = false;
		showMenu = false;
		
		world = new World();
	}
	
	public void start()
	{
		setUpObjects();
		setUpMenus();
		loadTextures();
		setUpCamera();
		setUpLighting();
		
		gameLoop();
	}
	
	private void gameLoop()
	{
		getDelta();
		lastFPS = getTime();
		
		player = world.getPlayer( username );
		
		while( !Display.isCloseRequested() )
		{
			checkCollision();
			render();
			checkInput();
			updateFPS();
			tick();
			Display.update();
			Display.sync( maxFPS );
		}
		
		cleanUp( false );
	}
	
	private void checkCollision()
	{		
		GameScreen.getClient().getSession().getActionSender().sendPosition( player );
	}
	
	private void render()
	{
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
		
		glLoadIdentity();
		
		camera.applyTranslations();
		
		render3D();
		render2D();
	}
	
	private void render3D()
	{
		glLight( GL_LIGHT0, GL_POSITION, BufferTools.asFlippedFloatBuffer( camera.x(), camera.y(), camera.z(), 1f ) );
		
		glMatrixMode( GL_PROJECTION );
		glPopMatrix();
		glLoadMatrix( perspectiveProjectionMatrix );
		glMatrixMode( GL_MODELVIEW );
		glEnable( GL_LIGHTING );
		glEnable( GL_DEPTH_TEST );
		
		for( Drawable drawObject : world.getDrawObjects() )
		{
			drawObject.render();
		}
	}
	
	private void render2D()
	{
		glDisable( GL_LIGHTING );
		glDisable( GL_DEPTH_TEST );
		glDisable( GL_CULL_FACE );
		
		glMatrixMode( GL_PROJECTION );
		glPushMatrix();
		glLoadIdentity();
		
		glLoadMatrix( orthographicProjectionMatrix );
		
		glMatrixMode( GL_MODELVIEW );
		glPushMatrix();
		glLoadIdentity();
		
		if( !showMenu )
		{
		
			if( drawFPS )
			{
				TextRenderer.getRenderer().render2DText( "FPS: " + currentFPS, 10, 10 );
				TextRenderer.getRenderer().render2DText( ( ( camera.isFollowing() )? "Following: " + camera.getFollowObject() : "Camera is not following" ) , 10, 30 );
				
			}
			
			renderHUD();
		}
		else
		{
			renderMenu();
		}
		
		glEnable( GL_LIGHTING );
		glEnable( GL_DEPTH_TEST );
		glEnable( GL_CULL_FACE );
		glMatrixMode( GL_PROJECTION );
		glPopMatrix();
		glMatrixMode( GL_MODELVIEW );
		glPopMatrix();
	}
	
	private void renderHUD()
	{
		//Drawing the bottom HUD
		Texture bottomHudTexture = TextureManager.getManager().getTexture( TextureType.HUD_BOTTOM );
		
		bottomHudTexture.bind();

		glBegin( GL_QUADS );
		{
			glColor4f( 0.5f, 0.5f, 0.5f, 1f );
			
			glTexCoord2f( 0f, 0f );
			glVertex2f( 0, ( Display.getHeight() - 100f ) );
			
			glTexCoord2f( bottomHudTexture.getWidth(), 0f );
			glVertex2f( Display.getWidth(), ( Display.getHeight() - 100f ) );
			
			glTexCoord2f( bottomHudTexture.getWidth(), bottomHudTexture.getHeight() );
			glVertex2f( Display.getWidth(), Display.getHeight() );
			
			glTexCoord2f( 0, bottomHudTexture.getHeight() );
			glVertex2f( 0, Display.getHeight() );
		}
		glEnd();
		
		TextRenderer.getRenderer().render2DText( "Cam[ x=" + formatter.format( camera.x() ) + ", y=" + formatter.format( camera.y() ) + ", z=" + formatter.format( camera.z() ) + " ]", 700, Display.getHeight() - 70 );
		TextRenderer.getRenderer().render2DText( "Cam[ pitch=" + formatter.format( camera.pitch() ) + ", yaw=" + formatter.format( camera.yaw() ) + ", roll=" + formatter.format( camera.roll() ) + " ]", 700, Display.getHeight() - 50 );
	}
	
	private void renderMenu()
	{
		MenuManager.getManager().getActiveMenu().render();
	}
	
	private void checkInput()
	{
		camera.processMouse( 1, 80, -80 );
		//player.processMouse( 1, 80, -80 );
		camera.processKeyboard( getDelta(), 1, 1, 1 );
		
		if( Mouse.isButtonDown( 0 ) )
		{
			if( !showMenu )
			{
				Mouse.setGrabbed( true );
			}
			else
			{
				MenuManager.getManager().checkButtons( new Rectangle( ( int ) Mouse.getX(), ( int ) Display.getHeight() - Mouse.getY(), 10, 10 ) );
			}
		}
		else if( Mouse.isButtonDown( 1 ) ) 
		{
			Mouse.setGrabbed( false );
		}
		
		while( Keyboard.next() )
		{
			//Show FPS
			if( Keyboard.isKeyDown( Keyboard.KEY_F1 ) )
			{
				drawFPS = !drawFPS;
			}
			if( Keyboard.isKeyDown( Keyboard.KEY_F2 ) )
			{
				//Reload the textures
				loadTextures();
			}
			if( Keyboard.isKeyDown( Keyboard.KEY_ESCAPE ) )
			{
				showMenu = !showMenu;
				Mouse.setGrabbed( ( showMenu )? false : true );
			}
			if( Keyboard.isKeyDown( Keyboard.KEY_P ) )
			{
				camera.pauseFollow();
				camera.setRotation( 0, 0, 0 );
			}
			if( Keyboard.isKeyDown( Keyboard.KEY_M ) )
			{
				showMenu = !showMenu;
				Mouse.setGrabbed( ( showMenu )? false : true );
			}
		}
	}
	
	private void updateFPS()
	{
		if( getTime() - lastFPS > 1000 )
		{
			currentFPS = fps;
			
			Display.setTitle( GameScreen.GAME_NAME + ", FPS: " + currentFPS );
			
			fps = 0;
			lastFPS += 1000;
		}
		
		fps++;
	}
	
	private void tick()
	{
		world.tick();
		camera.tick();
	}
	
	public String getUsername()
	{
		return username;
	}
	
	public void setUsername( String username )
	{
		this.username = username;
	}
	
	public World getWorld()
	{
		return world;
	}
	
	public long getTime()
	{
		return ( Sys.getTime() * 1000 ) / Sys.getTimerResolution();
	}
	
	public int getDelta()
	{
		long currentTime = getTime();
		int delta = ( int ) ( currentTime - lastFrame );
		lastFrame = getTime();
		
		return delta;
	}
	
	private void setUpObjects()
	{
		player = new Player( username, true );
		player.setPosition( new Vector3f( 0, 0, 0 ) );
		//player.colliding( false );
		//world.getDrawObjects().add( player );
		
		for( int y = 0; y < 1; y++ )
		{
			for( int x = 0; x < 20; x++ )
			{
				for( int z = 0; z < 20; z++ )
				{
					Cube cube = new Cube();
					cube.setPosition( new Vector3f( x * cube.getWidth(), y * cube.getHeight(), z * cube.getDepth() ) );
					cube.colliding( false );
					//world.getDrawObjects().add( cube );
				}
			}
		}
	}
	
	private void setUpMenus()
	{
		System.out.println( "loading menus..." );
		
		MainMenu mainmenu = new MainMenu();
		mainmenu.setX( 0 );
		mainmenu.setY( 0 );
		mainmenu.setWidth( Display.getWidth() );
		mainmenu.setHeight( Display.getHeight() );
		mainmenu.setR( 0 );
		mainmenu.setG( 0 );
		mainmenu.setB( 0 );
		mainmenu.setA( 1 );
		
		MenuButton btnExit = new MenuButton( ( Display.getWidth() / 2 ) - 100  , Display.getHeight() / 2, 200, 40, TextureType.BUTTON_EXIT, 1f, 0f, 0f, 1f );
		mainmenu.addItem( btnExit );
		
		MenuManager.getManager().addMenu( mainmenu, MenuType.MAIN_MENU );
		MenuManager.getManager().setActiveMenu( MenuType.MAIN_MENU );
	}
	
	private void loadTextures()
	{
		System.out.println( "loading Textures..." );
		
		TextureManager.getManager().addTexture( ImagingTools.loadTexture( "res/images/bottomHUD.png" ), TextureType.HUD_BOTTOM );
		TextureManager.getManager().addTexture( ImagingTools.loadTexture( "res/images/button_exit.png" ), TextureType.BUTTON_EXIT );
	}
	
	private void setUpCamera()
	{
		camera = new Camera( ( float ) Display.getWidth() / ( float ) Display.getHeight(), 0f, 0f, 0f );
		camera.applyPerspectiveMatrix();
		camera.followObject( player );
		camera.setFollowDistance( 0, 5f, 5f );
		glGetFloat( GL_PROJECTION_MATRIX, perspectiveProjectionMatrix );
		glMatrixMode( GL_PROJECTION );
		glLoadIdentity();
		glOrtho( 0, Display.getWidth(), Display.getHeight(), 0, 1, -1 );
		glGetFloat( GL_PROJECTION_MATRIX, orthographicProjectionMatrix );
		glLoadMatrix( perspectiveProjectionMatrix );
		glMatrixMode( GL_MODELVIEW_MATRIX );
	}
	
	private void setUpLighting()
	{
		glShadeModel( GL_SMOOTH );
		glEnable( GL_DEPTH_TEST );
		glEnable( GL_LIGHTING );
		glEnable( GL_TEXTURE_2D );
		glEnable( GL_BLEND );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
		glEnable(GL_LIGHT0);
        glLightModel( GL_LIGHT_MODEL_AMBIENT, BufferTools.asFlippedFloatBuffer( new float[] { 1f, 1f, 1f, 10f } ) );
        glLight( GL_LIGHT0, GL_POSITION, BufferTools.asFlippedFloatBuffer( new float[] { 1, 1, 1, 10 } ) );
		glEnable( GL_CULL_FACE );
		glCullFace( GL_BACK );
		glEnable( GL_COLOR_MATERIAL );
		glColorMaterial( GL_FRONT, GL_DIFFUSE );
	}
	
	public void cleanUp( boolean error )
	{
		System.out.println( "Cleaning up..." );
		
		for( int handle : world.getHandles() )
		{
			glDeleteBuffers( handle );
		}
		
		Display.destroy();
		System.exit( ( error )? 1 : 0 );
	}
}
